﻿using System;
using System.Linq;
using System.Collections.Generic;

using StackExchange.Redis;
using TrinityAPI.Base;
using System.Data.Common;

namespace TrinityAPI.Cache.Redis
{
    public class CacheHelper : ICache
    {
        private int _db = -1;
        private static int _poolSize = 100;
        private static object lockHelper = new object();
        private static Dictionary<string, CacheManager> CacheManagers = new Dictionary<string, CacheManager>();
        private CacheManager CacheManager;

        public CacheHelper(string connString)
        {
            Init(connString);
        }

        public CacheHelper(string connString, int database) : this(connString)
        {
            _db = database < -1 ? -1 : database;
        }

        private void Init(string connectionString)
        {
            if (string.IsNullOrWhiteSpace(connectionString))
                throw new Exception("Cannot configure redis with an empty connection string.");

            if (!CacheManagers.ContainsKey(connectionString) || !CacheManagers[connectionString].RedisConnection.IsConnected)
            {
                lock (lockHelper)
                {
                    if (!CacheManagers.ContainsKey(connectionString) || !CacheManagers[connectionString].RedisConnection.IsConnected)
                    {
                        if (CacheManagers.ContainsKey(connectionString))
                        {
                            CacheManagers[connectionString].Dispose();
                            CacheManagers.Remove(connectionString);
                        }

                        try
                        {
                            CacheManager = new CacheManager(ConnectionMultiplexer.Connect(connectionString));
                        }
                        catch (Exception ex)
                        {
                            throw new Exception("Cannot connect to the redis server: " + ex.Message);
                        }
                        CacheManagers[connectionString] = CacheManager;
                    }
                    else
                    {
                        CacheManager = CacheManagers[connectionString];
                    }
                }
            }
            else
            {
                CacheManager = CacheManagers[connectionString];
            }

            CacheManager.References++;

            if (CacheManagers.Count() > _poolSize)
            {
                PurgePool();
            }
        }

        private void PurgePool()
        {
            System.Threading.Tasks.Task.Run(() =>
            {
                int index = 0;
                var sortedList = CacheManagers.OrderByDescending(i => i.Value.References).ThenByDescending(i => i.Value.LastVisitTime);
                foreach (var item in sortedList)
                {
                    index++;
                    if (item.Value.References > 0)
                        continue;
                    if (index > _poolSize)
                    {
                        CacheManagers[item.Key].Dispose();
                        CacheManagers.Remove(item.Key);
                    }
                }
            });
        }

        ~CacheHelper()
        {
            if (CacheManager != null)
            {
                CacheManager.References--;
            }
        }

        public object Get(string key)
        {
            object value = null;
            byte[] bytes = CacheManager.RedisConnection.GetDatabase(_db).StringGet(key);
            if (bytes != null && bytes.Length > 1)
            {
                SerializedType type = (SerializedType)Enum.Parse(typeof(SerializedType), bytes[0].ToString());
                byte[] data = new byte[bytes.Length - 1];
                Buffer.BlockCopy(bytes, 1, data, 0, data.Length);
                value = Serializer.DeSerialize(data, type);
            }
            return value;
        }

        public void Set(string key, object value)
        {
            if (value != null)
            {
                SerializedType type;
                byte[] bytes;
                byte[] data = Serializer.Serialize(value, out type, 1024 * 128);
                bytes = new byte[data.Length + 1];
                bytes[0] = (byte)type.GetHashCode();
                Buffer.BlockCopy(data, 0, bytes, 1, data.Length);
                CacheManager.RedisConnection.GetDatabase(_db).StringSet(key, bytes);
            }
        }

        public void Set(string key, object value, int timeout)
        {
            if (value != null)
            {
                Set(key, value);
                CacheManager.RedisConnection.GetDatabase(_db).KeyExpire(key, DateTime.Now.AddSeconds(timeout));
            }
        }

        public void Remove(string key)
        {
            CacheManager.RedisConnection.GetDatabase(_db).KeyDelete(key);
        }

        public void RemoveAll()
        {
            var endpoints = CacheManager.RedisConnection.GetEndPoints(true);
            foreach (var endpoint in endpoints)
            {
                var server = CacheManager.RedisConnection.GetServer(endpoint);
                server.FlushDatabase();
            }
        }
    }
}
